﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;
using Utility.Math;

namespace Utility.NodeGraphs
{
	[Serializable]
	public class NodePort
	{
		public static NodePort New (Node node, string name = "Port")
		{
			return new NodePort (node, name);
		}

		protected NodePort (Node node, string name)
		{
			_node = node;
			this.name = name;
		}

		[SerializeField] Node _node;
		public Node Node => _node;

		[SerializeField] int _index = -1;
		public int Index
		{
			get
			{
				if (_index < 0)
				{
					_index = Node.GetPortIndex (this);
				}
				return _index;
			}
		}

		public string name;
		public Rect rect;//local to node
		public Vector2 dirPoint;//for draw bezier curve, local to the port center
		public int inputLimit = -1;
		public int outputLimit = -1;
		public float weight = 1;

		[SerializeField]
		public List<NodePortOutput> outputs = new List<NodePortOutput> ();
		[SerializeField]
		public List<NodePortInput> inputs = new List<NodePortInput> ();

		public int OutputCount => outputs.Count;
		public int InputCount => inputs.Count;
		public int ConnectionCount => InputCount + OutputCount;

		public bool ReachOutputLimit => outputLimit >= 0 && outputLimit <= OutputCount;
		public bool ReachInputLimit => inputLimit >= 0 && inputLimit <= InputCount;

		public bool CanBeRemoved => Node.CanRemovePort (Index);

		public Vector2 CanvasCenter => rect.center + Node.CanvasPos;
		public Vector2 CanvasTangentPoint => CanvasCenter + dirPoint;
		public Rect CanvasRect => rect.Shift (Node.CanvasPos);

		public Node.PortOnMaxConnectionBehaviours OnMaxConnectionBehaviour => Node.GetPortOnMaxConnectionBehaviour (Index);

		public bool HasConnectionTo (NodePort targetPort) => outputs.Any (output => output.TargetPort == targetPort);
		public bool HasConnectionTo (Node targetNode) => outputs.Any (output => output.targetNode == targetNode);

		public bool HasConnectionFrom (NodePort sourcePort) => inputs.Any (input => input.SourcePort == sourcePort);
		public bool HasConnectionFrom (Node sourceNode) => inputs.Any (input => input.sourceNode == sourceNode);

		public bool ConnectTo (NodePort targetPort)
		{
			if (CanConnectTo (targetPort) && targetPort.CanConnectFrom (this))
			{
				NodePortOutput output = new NodePortOutput (this, targetPort);
				outputs.Add (output);

				NodePortInput input = new NodePortInput (this);
				targetPort.inputs.Add (input);

				Node.OnConnectionsChange ();
				return true;
			}
			else
			{
				return false;
			}
		}

		public void RemoveConnections ()
		{
			RemoveInputs ();
			RemoveOutputs ();
		}

		public void RemoveInputs ()
		{
			bool changed = inputs.Count > 0;
			foreach (var input in inputs)
			{
				input.SourcePort.outputs.RemoveAll (output => output.TargetPort == this);
			}
			inputs.Clear ();

			if (changed)
			{
				Node.OnConnectionsChange ();
			}
		}

		public void RemoveOutputs ()
		{
			bool changed = outputs.Count > 0;
			foreach (var output in outputs)
			{
				output.TargetPort.inputs.RemoveAll (input => input.SourcePort == this);
			}
			outputs.Clear ();

			if (changed)
			{
				Node.OnConnectionsChange ();
			}
		}

		public void RemoveInput (int index)
		{
			if (index < 0 || index >= inputs.Count)
			{
				return;
			}

			inputs[index].SourcePort.outputs.RemoveAll (output => output.TargetPort == this);
			inputs.RemoveAt (index);

			Node.OnConnectionsChange ();
		}

		public void RemoveOutput (int index)
		{
			if (index < 0 || index >= outputs.Count)
			{
				return;
			}

			outputs[index].TargetPort.inputs.RemoveAll (input => input.SourcePort == this);
			outputs.RemoveAt (index);

			Node.OnConnectionsChange ();
		}

		public bool ExistInputFrom (NodePort port)
		{
			return inputs.Any (input => input.SourcePort == port);
		}

		public bool ExistOutputTo (NodePort port)
		{
			return outputs.Any (output => output.TargetPort == port);
		}

		public virtual bool CanConnectTo (NodePort target) => Node.PortCanConnectTo (Index, target);
		public virtual bool CanConnectFrom (NodePort source) => Node.PortCanConnectFrom (Index, source);

		public int GetIndexOfInputFrom (NodePort sourcePort)
		{
			return inputs.FindIndex (input => input.SourcePort == sourcePort);
		}

		public int GetIndexOfOutputTo (NodePort targetPort)
		{
			return outputs.FindIndex (output => output.TargetPort == targetPort);
		}

		//called before port change index
		public void OnChangeIndex (int newIndex)
		{
			foreach (var input in inputs)
			{
				int outputIndex = input.SourcePort.GetIndexOfOutputTo (this);
				var output = input.SourcePort.outputs[outputIndex];
				input.SourcePort.outputs[outputIndex] = new NodePortOutput ()
				{
					targetNode = Node,
					targetPortIndex = newIndex,
					outDirPoint = output.outDirPoint,
					inDirPoint = output.inDirPoint,
				};
			}
			foreach (var output in outputs)
			{
				int inputIndex = output.TargetPort.GetIndexOfInputFrom (this);
				output.TargetPort.inputs[inputIndex] = new NodePortInput ()
				{
					sourceNode = Node,
					sourcePortIndex = newIndex,
				};
			}

			_index = -1;
		}

	}

	[Serializable]
	public struct NodePortOutput
	{
		public Node targetNode;
		public int targetPortIndex;

		public NodePort TargetPort => targetNode.GetPort (targetPortIndex);

		public Vector2 outDirPoint;
		public Vector2 inDirPoint;

		public NodePortOutput (NodePort port, NodePort targetPort)
		{
			targetNode = targetPort.Node;
			targetPortIndex = targetNode.GetPortIndex (targetPort);

			outDirPoint = port.dirPoint;
			inDirPoint = targetPort.dirPoint;
		}
	}

	[Serializable]
	public struct NodePortInput
	{
		public Node sourceNode;
		public int sourcePortIndex;
		public NodePort SourcePort => sourceNode.GetPort (sourcePortIndex);

		public NodePortInput (NodePort sourcePort)
		{
			sourceNode = sourcePort.Node;
			sourcePortIndex = sourceNode.GetPortIndex (sourcePort);
		}

	}
}